package shawn.projection;

public class GoogleProjection implements Projection {

  // http://msdn.microsoft.com/en-us/library/bb259689.aspx
  private static double EarthRadius = 6378137;
  private static double MinLnggitude = -180;
  private static double MaxLnggitude = 180;
  private static double MinLatitude = -85.05112878;
  private static double MaxLatitude = 85.05112878;

  public String getName() {
    return "Google";
  }

  public LngLatPoint lnglatToMercator(LngLatPoint llP, int level) {
    long mapSize = mapSize(level);

    double tlng = getRange(llP.lng, MinLnggitude, MaxLnggitude);
    double x = (tlng + 180) / 360;
    x = getRange(x * mapSize + 0.5, 0, mapSize - 1);

    double tlat = getRange(llP.lat, MinLatitude, MaxLatitude);
    double sinLatitude = Math.sin(tlat * Math.PI / 180);
    double y = 0.5 - Math.log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * Math.PI);
    y = getRange(y * mapSize + 0.5, 0, mapSize - 1);

    return new LngLatPoint((long) x, (long) y);
  }

  public LngLatPoint mercatorToLngLat(LngLatPoint mcP, int level) {
    long mapSize = mapSize(level);

    double lng = (getRange(mcP.lng, 0, mapSize - 1) / mapSize) - 0.5;
    lng = 360 * lng;

    double lat = 0.5 - (getRange(mcP.lat, 0, mapSize - 1) / mapSize);
    lat = 90 - 360 * Math.atan(Math.exp(-lat * 2 * Math.PI)) / Math.PI;

    return new LngLatPoint(lng, lat);
  }

  public PixelPoint lnglatToPixel(LngLatPoint llp, int level) {
    LngLatPoint mcP = lnglatToMercator(llp, level);
    return new PixelPoint((long) mcP.lng, (long) mcP.lat);
  }

  public LngLatPoint pixelToLngLat(PixelPoint pp, int level) {
    LngLatPoint mcP = new LngLatPoint(pp.x, pp.y);
    return mercatorToLngLat(mcP, level);
  }

  private double getRange(double n, double min, double max) {
    return Math.min(Math.max(n, min), max);
  }

  private double getLoop(double n, double min, double max) {
    double r = n;
    while (r > max) {
      r -= max - min;
    }
    while (r < min) {
      r += max - min;
    }
    return r;
  }

  public long mapSize(int levelOfDetail) {
    // return 256 << levelOfDetail;
    return (long) (256 * Math.pow(2, levelOfDetail));
  }

  public double groundResolution(double latitude, int levelOfDetail) {
    double tl = getRange(latitude, MinLatitude, MaxLatitude);
    return Math.cos(tl * Math.PI / 180) * 2 * Math.PI * EarthRadius / mapSize(levelOfDetail);
  }

  public double mapScale(double latitude, int levelOfDetail, int screenDpi) {
    return groundResolution(latitude, levelOfDetail) * screenDpi / 0.0254;
  }

  public PixelPoint mercatorToPixel(LngLatPoint mcP, int level) {
    return new PixelPoint((long) mcP.lng, (long) mcP.lat);
  }

  public LngLatPoint pixelToMercator(PixelPoint pP, int level) {
    return new LngLatPoint(pP.x, pP.y);
  }

  public TilePoint lnglatToTile(LngLatPoint llP, int level) {
    LngLatPoint mcP = lnglatToMercator(llP, level);
    return mercatorToTile(mcP, level);
  }

  public TilePoint mercatorToTile(LngLatPoint mcP, int level) {
    int tileX = (int) mcP.lng / 256;
    int tileY = (int) mcP.lat / 256;
    return new TilePoint(tileX, tileY);
  }

  public TilePoint pixelToTile(PixelPoint pP, int level) {
    int tileX = (int) pP.x / 256;
    int tileY = (int) pP.y / 256;
    return new TilePoint(tileX, tileY);
  }

  public PixelPoint upperLeftOfTile(TilePoint t, int level) {
    return new PixelPoint(t.x * 256, t.y * 256);
  }

  public PixelPoint lowerRightOfTile(TilePoint t, int level) {
    return new PixelPoint((t.x + 1) * 256 - 1, (t.y + 1) * 256 - 1);
  }

  public double getDistanceByMC(LngLatPoint p1, LngLatPoint p2) {
    if (p1 == null || p2 == null) {
      return 0;
    }
    LngLatPoint tp1 = this.mercatorToLngLat(p1, -1);
    double lng1 = this.toRadians(tp1.lng);
    double lat1 = this.toRadians(tp1.lat);
    LngLatPoint tp2 = this.mercatorToLngLat(p2, -1);
    double lng2 = this.toRadians(tp2.lng);
    double lat2 = this.toRadians(tp2.lat);
    return getDistance(lng1, lng2, lat1, lat2);
  }

  public double getDistanceByLL(LngLatPoint p1, LngLatPoint p2) {
    if (p1 == null || p2 == null) {
      return 0;
    }
    double lng1 = getLoop(p1.lng, MinLnggitude, MaxLnggitude);
    double lat1 = getRange(p1.lat, MinLatitude, MaxLatitude);
    double lng2 = getLoop(p2.lng, MinLnggitude, MaxLnggitude);
    double lat2 = getRange(p2.lat, MinLatitude, MaxLatitude);

    lng1 = toRadians(lng1);
    lat1 = toRadians(lat1);
    lng2 = toRadians(lng2);
    lat2 = toRadians(lat2);

    return getDistance(lng1, lng2, lat1, lat2);
  }

  private double toRadians(double a) {
    return Math.PI * a / 180;
  }

  private double getDistance(double radiansLng1, double radiansLng2, double radiansLat1, double radiansLat2) {
    return EarthRadius * Math.acos((Math.sin(radiansLat1) * Math.sin(radiansLat2) + Math.cos(radiansLat1) * Math.cos(radiansLat2) * Math.cos(radiansLng2 - radiansLng1)));
  }

  public PixelPoint gridToPixel(Grid grid, int level) {
    LngLatPoint llEquatorOrigin = new LngLatPoint(0, 0);
    PixelPoint pEquatorOrigin = lnglatToPixel(llEquatorOrigin, 18);
    PixelPoint p18 = new PixelPoint(Math.round(grid.x * 256 + 127) + pEquatorOrigin.x, pEquatorOrigin.y - Math.round(grid.y * 256 + 127));
    LngLatPoint llp = pixelToLngLat(p18, 18);
    return lnglatToPixel(llp, level);
  }

  public Grid pixelToGrid(PixelPoint pP, int level) {
    LngLatPoint llEquatorOrigin = new LngLatPoint(0, 0);
    PixelPoint pEquatorOrigin = lnglatToPixel(llEquatorOrigin, 18);
    LngLatPoint llp = pixelToLngLat(pP, level);
    PixelPoint p18 = lnglatToPixel(llp, 18);
    Grid grid = new Grid();
    grid.x = (p18.x - pEquatorOrigin.x - 127) / 256d;
    grid.y = (pEquatorOrigin.y - p18.y - 127) / 256d;
    return grid;
  }

  public int getGridWidthHeight(int level) {
    return 256 / getZoomUnits(level);
  }

  private int getZoomUnits(int level) {
    return (int) Math.pow(2, (18 - level));
  }

  public static void main(String[] args) {
    GoogleProjection p = new GoogleProjection();

    LngLatPoint p1 = new LngLatPoint(116.35553, 39.937999);
    System.out.println(p1.lng + " | " + p1.lat);

    PixelPoint p4 = p.lnglatToPixel(p1, 16);
    System.out.println(p4.x + " | " + p4.y);
    LngLatPoint p5 = p.pixelToLngLat(p4, 16);
    System.out.println(p5.lng + " | " + p5.lat);

    TilePoint t = p.lnglatToTile(p1, 16);
    System.out.println(t);
    // 116.35553 | 39.937999
    // 13811169 | 6355275
    // 116.3555359840393 | 39.937990918300855

    Grid grid = new Grid(0, 0);
    PixelPoint pp = p.gridToPixel(grid, 18);
    LngLatPoint llp = p.pixelToLngLat(pp, 18);
    Grid g = p.pixelToGrid(pp, 18);

    System.out.println(pp);
    System.out.println(llp);
    System.out.println(g);

  }

}
